#include "DALFileAccess.h"
#include "DALUtils.h"
#include "uthash.h"

#define RECORD_DELIMETER 255
#define BUFFER_SIZE 16384 //16 KB of data should suffice
#define MAXIMAL_FILEPATH 1024 //1 KB of data should suffice
#define BASE_EXTENSION ".base"
#define APPEND_EXTENSION ".append"
#define TEMP_EXTENSION ".tempbase"

//Local buffer used for memory purposes
char BUFFER[BUFFER_SIZE];

void ClearBuffer()
{
	memset(&BUFFER, 0, BUFFER_SIZE);
}

typedef struct
{
	FILE* BaseFile;
	FILE* AppendFile;
} EntityFileEnvironment;

typedef struct
{
	unsigned int ID;
	unsigned int Bytes;
	char* Data;
	UT_hash_handle hh;
} FileRecord;


FILE* ReadFile(unsigned int EntityTypeIndex, const char* extension)
{
	char FILEPATH[MAXIMAL_FILEPATH] = {0};
	char strIndex[3] = {0};
	FILE* file;
	//Get the string form of the index
	sprintf(strIndex, "%d", EntityTypeIndex);

	/*Directory path ends with '\', e.g.: C:\TEST\*/
	strcpy(FILEPATH, EnvironmentDirectoryPath);

	//Concat with index and extension to get the resultant filepath
	strcat(FILEPATH, strIndex);
	strcat(FILEPATH, extension);

	//Open the file
	file = fopen(FILEPATH, "r");
	if (file)
		return file;

	//File doesn't exist, create it, close it, open it again
	file = fopen(FILEPATH, "w");
	fclose(file);
	file = fopen(FILEPATH, "r");

	return file;
}

FILE* WriteFile(unsigned int EntityTypeIndex, const char* extension)
{
	char FILEPATH[MAXIMAL_FILEPATH] = { 0 };
	char strIndex[3] = { 0 };
	FILE* file;
	//Get the string form of the index
	sprintf(strIndex, "%d", EntityTypeIndex);

	/*Directory path ends with '\', e.g.: C:\TEST\*/
	strcpy(FILEPATH, EnvironmentDirectoryPath);

	//Concat with index and extension to get the resultant filepath
	strcat(FILEPATH, strIndex);
	strcat(FILEPATH, extension);

	//Open the file for writing
	file = fopen(FILEPATH, "w+");

	return file;
}

//Returns 0 if there are no more records, otherwise returns 1
int ReadBaseRecord(FILE* file, FileRecord* outRecord)
{
	int i = 0;
	int c = fgetc(file);
	//Check if the file is in EOF
	if (c == EOF)
		return 0;

	//Revert
	ungetc(c, file);

	//Read ID
	int ID;
	fscanf(file, "%u", &ID);

	outRecord->ID = ID;
	
	//Clear the buffer just in case
	ClearBuffer();

	//Read the rest of the data until a delimiter (or EOF, but that shouldn't happen) is reached
	c = 0;
	while (c != RECORD_DELIMETER && c != EOF)
	{
		c = fgetc(file);
		BUFFER[i] = c;
		i++;
	}

	//Allocate memory for the data
	outRecord->Data = malloc(i-1);
	outRecord->Bytes = i-1;

	//Copy memory for the data
	memcpy(outRecord->Data, BUFFER, i-1);

	return 1;
}

//Returns 0 if there are no more records, otherwise returns -1 if there's a record to edit or add, or returns a positive number if there's a record to delete
long ReadAppendRecord(FILE* file, FileRecord* outRecord)
{
	int i = 0, ID = 0;
	int c = fgetc(file);
	//Check if the file is in EOF
	if (c == EOF)
		return 0;

	//Revert
	ungetc(c, file);

	//Check if the record is a delete record
	c = fgetc(file);

	//This is a delete record - return the index to delete
	if (c)
	{
		int DeleteID;
		fscanf(file, "%u", &DeleteID);
		outRecord->ID = 0;
		outRecord->Data = NULL;
		//Return the ID
		return DeleteID;
	}
	//This is a modify/add record, read the appropriate data
	else
	{
		//Clear the buffer just in case
		ClearBuffer();

		//Read ID
		fscanf(file, "%u", &ID);

		outRecord->ID = ID;
		
		c = 0;
		while (c != RECORD_DELIMETER && c != EOF)
		{
			c = fgetc(file);
			BUFFER[i] = c;
			i++;
		}

		//Allocate memory for the data
		outRecord->Data = malloc(i-1);
		outRecord->Bytes = i-1;

		//Copy memory for the data
		memcpy(outRecord->Data, BUFFER, i-1);
	}

	return -1;
}

void WriteBaseRecord(FILE* file, FileRecord* Record)
{
	if (Record->ID == 0)
		return;
	//Write ID
	fprintf(file, "%u", Record->ID);
	//Write Data
	for (unsigned int i = 0; i < Record->Bytes; i++)
		fputc(Record->Data[i], file);

	//Add delimiter
	fputc(RECORD_DELIMETER, file);

	fflush(file);
}

void DeleteBaseRecord(FileRecord* Record)
{
	Record->Bytes = 0;
	free(Record->Data);
	Record->Data = NULL;
}

int id_sort(FileRecord *a, FileRecord *b)
{
	return (a->ID - b->ID);
}

void FinalizeEnvironment(unsigned int EntityTypeIndex)
{
	char FILEPATH[MAXIMAL_FILEPATH] = { 0 };
	char strIndex[3] = { 0 };
	//Get the string form of the index
	sprintf(strIndex, "%d", EntityTypeIndex);

	/*Directory path ends with '\', e.g.: C:\TEST\*/
	strcpy(FILEPATH, EnvironmentDirectoryPath);

	//Concat with index
	strcat(FILEPATH, strIndex);
	
	char BASE_FILEPATH[MAXIMAL_FILEPATH] = { 0 };
	char APPEND_FILEPATH[MAXIMAL_FILEPATH] = { 0 };
	char TEMP_FILEPATH[MAXIMAL_FILEPATH] = { 0 };

	strcpy(BASE_FILEPATH, FILEPATH);
	strcpy(APPEND_FILEPATH, FILEPATH);
	strcpy(TEMP_FILEPATH, FILEPATH);

	strcat(BASE_FILEPATH, BASE_EXTENSION);
	strcat(APPEND_FILEPATH, APPEND_EXTENSION);
	strcat(TEMP_FILEPATH, TEMP_EXTENSION);

	//Delete the two files and rename the temporary file
	remove(BASE_FILEPATH);
	remove(APPEND_FILEPATH);
	rename(TEMP_FILEPATH, BASE_FILEPATH);

	//Finalize environment
	DALFileEnvironment[EntityTypeIndex] = malloc(sizeof(EntityFileEnvironment));
	EntityFileEnvironment* EFE = DALFileEnvironment[EntityTypeIndex];

	EFE->AppendFile = fopen(APPEND_FILEPATH, "w+");
	EFE->BaseFile = fopen(BASE_FILEPATH, "r");
}

int EstablishEntityFileEnvironment(unsigned int EntityTypeIndex)
{
	int appID; //Append ID for deletion

	//! Create hashtable for all records
	FileRecord* RecordsHT = NULL;
	FileRecord fileRecord;

	//! Read BaseFile if it exists (create if it doesn't exist), add records in file to the hashtable
	FILE* baseFile = ReadFile(EntityTypeIndex, BASE_EXTENSION);
	
	while (ReadBaseRecord(baseFile, &fileRecord))
	{
		FileRecord* thisRecord;
		HASH_FIND_INT(RecordsHT, &fileRecord.ID, thisRecord);
		///If we've found an entity, free the previous data and use the existing record (NOTE: THIS SHOULD NEVER REALLY HAPPEN)
		if (thisRecord)
			DeleteBaseRecord(thisRecord);
		//We haven't found an existing entity. Create a new entity
		else
		{
			thisRecord = (FileRecord*)malloc(sizeof(FileRecord));
			thisRecord->ID = fileRecord.ID;
			HASH_ADD_INT(RecordsHT, ID, thisRecord);
		}

		//NOTE: Memory responsibility moved to the hash table
		thisRecord->Bytes = fileRecord.Bytes;
		thisRecord->Data = fileRecord.Data;

		int debug = 0;;
	}

	fclose(baseFile);

	//! Read AppendFile if it exists (create if it doesn't exist), add/edit records in file to the hashtable
	FILE* appFile = ReadFile(EntityTypeIndex, APPEND_EXTENSION);
	while (appID = ReadAppendRecord(appFile, &fileRecord))
	{
		//This is a modify record
		if (appID == -1)
		{
			FileRecord* thisRecord;
			HASH_FIND_INT(RecordsHT, &fileRecord.ID, thisRecord);
			///If we've found an entity, free the previous data and use the existing record
			if (thisRecord)
				DeleteBaseRecord(thisRecord);
			//We haven't found an existing entity. Create a new entity
			else
			{
				thisRecord = (FileRecord*)malloc(sizeof(FileRecord));
				memset(thisRecord, 0, sizeof(FileRecord));
				thisRecord->ID = fileRecord.ID;
				HASH_ADD_INT(RecordsHT, ID, thisRecord);
			}

			//NOTE: Memory responsibility moved to the hash table
			thisRecord->Bytes = fileRecord.Bytes;
			thisRecord->Data = fileRecord.Data;
		}
		//This is a delete record
		else
		{
			FileRecord* thisRecord;
			HASH_FIND_INT(RecordsHT, &((unsigned int)appID), thisRecord);
			///If we've found an entity, delete it from the hashtable, and free its associated memory
			if (thisRecord)
			{
				HASH_DEL(RecordsHT, thisRecord);
				DeleteBaseRecord(thisRecord);
			}
		}
	}

	fclose(appFile);

	//! Sort hashtable by ID
	HASH_SORT(RecordsHT, id_sort);

	//! Write records in hashtable to a new temp BaseFile
	FILE* newFile = WriteFile(EntityTypeIndex, TEMP_EXTENSION);
	FileRecord *current_entity, *tmp;
	HASH_ITER(hh, RecordsHT, current_entity, tmp)
	{
		//Write record to the new file
		WriteBaseRecord(newFile, current_entity);
		//Delete record from HT
		HASH_DEL(RecordsHT, current_entity);
		//Free record's memory
		DeleteBaseRecord(current_entity);
	}

	//! Close the file
	fclose(newFile);

	//! Delete old base file and append file
	//! Rename the new BaseFile to be the current BaseFile
	//! Open BaseFile for reading (and set it to the appropriate struct)
	//! Open AppendFile for writing while deleting everything (and set it to the appropriate struct)
	FinalizeEnvironment(EntityTypeIndex); //This function basically does all the above
	
	return 0;
}

void FreeEntityFileEnvironmnet(unsigned int EntityTypeIndex)
{
	EntityFileEnvironment* EFE = DALFileEnvironment[EntityTypeIndex];
	fclose(EFE->AppendFile);
	fclose(EFE->BaseFile);

	free(DALFileEnvironment[EntityTypeIndex]);
}

int EstablishFileSystem()
{
	unsigned int i;

	//TODO: Create root directory if necessary

	for (i = 0; i < EnvironmentEntityTable.Size; i++)
		EstablishEntityFileEnvironment(i);

	return 0;
}

void FreeFileSystem()
{
	unsigned int i;

	for (i = 0; i < EnvironmentEntityTable.Size; i++)
		FreeEntityFileEnvironmnet(i);
}


//Append file related functions:

int CreateOrEditEntityInFile(unsigned int EntityTypeIndex, unsigned int ID, DataEntity DEntity)
{
	EntityFileEnvironment* EFE = DALFileEnvironment[EntityTypeIndex];

	//Indicate a record create/edit
	fputc(0, EFE->AppendFile);

	//Write ID
	fprintf(EFE->AppendFile, "%u", ID);

	//Write Data
	for (unsigned int i = 0; i < DEntity.Bytes; i++)
		fputc(DEntity.Data[i], EFE->AppendFile);

	//Add delimeter
	fputc(RECORD_DELIMETER, EFE->AppendFile);

	fflush(EFE->AppendFile);

	return 0;
}

int DeleteEntityInFile(unsigned int EntityTypeIndex, unsigned int ID)
{
	EntityFileEnvironment* EFE = DALFileEnvironment[EntityTypeIndex];

	//Indicate a record delete
	fputc(1, EFE->AppendFile);

	//Write ID
	fprintf(EFE->AppendFile, "%u", ID);

	return 0;
}


//Base file related functions:

DataEntity GetEntityInFile(unsigned int EntityTypeIndex, unsigned int ID)
{
	int RecordFound = 0;
	EntityFileEnvironment* EFE = DALFileEnvironment[EntityTypeIndex];

	DataEntity DE;
	DE.Bytes = 0;
	DE.Data = 0;

	//Rewind to read from the beginning
	rewind(EFE->BaseFile);

	FileRecord fileRecord;
	fileRecord.Bytes = 0;
	fileRecord.Data = NULL;

	while (ReadBaseRecord(EFE->BaseFile, &fileRecord))
	{
		if (fileRecord.ID == ID)
		{
			RecordFound = 1;
			break;
		}

		DeleteBaseRecord(&fileRecord);
	}

	//We've found a record
	if (RecordFound)
	{
		DE.Bytes = fileRecord.Bytes;

		//Transfer memory responsibility
		DE.Data = fileRecord.Data;
	}

	return DE;
}